<?php
class Database
{
    private $_sql;
    private $_bind;
    private $_last_exception = FALSE;
    private static $_instance = FALSE;

    public static function check($config)
    {
        $instance = new Database($config);

        if ($instance->_last_exception === FALSE) {
            return true;
        } else {
            return $instance->_last_exception;
        }
    }

    public static function getPdoInstance()
    {
        return self::$_instance;
    }

    public function __construct($config = FALSE)
    {
        $config === FALSE && $config = Loader::getConfig('database', 'current');
        $options = array(
            # Sử dụng chế độ ngoại lệ
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            # Bật kết nỗi mã hóa UTF-8
            PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'UTF8'"
        );

        $dsn = "mysql:host={$config['host']};dbname={$config['name']}";

        try {
            # Thực hiện mở kết nối
            self::$_instance = new PDO($dsn, $config['user'], $config['pass'], $options);
        } catch (Exception $ex) {
            if ($ex->getCode() == 1045) {
                $this->_last_exception = new Exception(
                    'Truy cập dữ liệu bị từ chối, sai tên đăng nhập / mật khẩu', 1045);
            } else {
                # Nếu lỗi chưa biết
                $this->_last_exception = $ex;
            }
        }
    }

    /**
     * Trả về một mảng được tạo từ biến $bind
     * @param $bind
     * @return array
     */
    protected function cleanup($bind)
    {
        if (!is_array($bind)) {
            if (!empty($bind)) {
                $bind = array($bind);
            } else {
                $bind = array();
            }
        }

        return $bind;
    }

    /**
     * Thực hiện câu lệnh sql
     * @param $sql
     * @param string $bind
     * @return array|bool|int
     */
    public function run($sql, $bind = '', $mode = PDO::FETCH_ASSOC)
    {
        $this->_sql = trim($sql);
        $this->_bind = $this->cleanup($bind);

        $pdoStmt = self::$_instance->prepare($this->_sql);

        // Thực thi thành công, trả về dữ liệu
        if ($pdoStmt->execute($this->_bind) !== false) {
            // Nếu là SQL dạng select, describe
            if (preg_match('/^(select|describe|show)/i', $this->_sql)) {
                return $pdoStmt->fetchAll($mode);
            } elseif (preg_match('/^(delete|insert|update)/i', $this->_sql)) {
                return $pdoStmt->rowCount();
            }
        }
        return false;
    }

    /**
     * Lấy danh sách cột hợp lệ trong bảng $table và mảng $info
     * @param $table
     * @param $info
     * dạng array (
     *  column => value,
     * ...
     * )
     * @return array
     */
    protected  function filter($table, $info)
    {
        $sql = "DESCRIBE {$table};";
        if (($list = $this->run($sql)) !== false) {
            $fields = array();
            foreach ($list as $record) {
                $fields[] = $record['Field'];
            }
            return array_values(array_intersect($fields, array_keys($info)));
        }
        return array();
    }

    /**
     * Thực hiện truy vấn trả về giá trị đơn
     * @param $sql
     * @param string $bind
     * @return null
     */
    public function executeScalar($sql, $bind ='')
    {
        $result = $this->run($sql, $bind);
        if ($result !== false && count($result) > 0) {
            $firstRow = $result[0];
            $cols = array_keys($firstRow);
            return $firstRow[$cols[0]];
        }

        return null;
    }

    /**
     * Lấy dữ liệu từ bảng dữ liệu, hỗ trợ giới hạn số mẫu tin
     * @param string $table
     * @param string $where
     * @param array $bind
     * @param string $fields
     * @param int $limit
     * @param int $count
     * @return array|bool
     */
    public function select($table, $where = null, $bind = null, $fields = '*', $limit = null, $count = 10)
    {
        $sql = "SELECT {$fields} FROM {$table}";
        if (!empty($where)) {
            $sql .= " WHERE {$where}";
        }

        if ($limit && $count && is_numeric($limit) && is_numeric($count)) {
            $sql .= " LIMIT {$limit},{$count}";
        }

        $sql .= ';';

        return $this->run($sql, $bind);
    }


    /**
     * Cập nhật dữ liệu vào bảng
     * @param $table
     * @param $info array (assoc array) dạng [column => value, column => value, ...]
     * @param $where string mệnh đề điều kiện
     * @param $bind array giá trị cho các parameter, assoc array
     * @return bool|int
     */
    public function update($table, $info, $where, $bind = null)
    {
        $fields = $this->filter($table, $info);
        $sql = "UPDATE {$table} SET";
        foreach ($fields as $field) {
            $sql .= " {$field} = :update_{$field},";
        }
        $sql = preg_replace('/^(.+),$/', '$1', $sql);
        $sql .= " WHERE {$where};";

        $bind = $this->cleanup($bind);
        foreach ($fields as $field) {
            $bind[":update_{$field}"] = $info[$field];
        }
        return $this->run($sql, $bind);
    }

    /**
     * Thêm dữ liệu vào cơ sở dữ liệu
     * @param $table string
     * @param $info array (assoc array)
     * @return bool|int
     */
    public function insert($table, $info)
    {
        $fields = $this->filter($table, $info);
        $sql = sprintf(
            "INSERT INTO {$table}(%s) VALUES(:%s)",
            implode($fields, ', '), implode($fields, ', :')
        );
        $bind = array();
        foreach ($fields as $field) {
            $bind[":{$field}"] = $info[$field];
        }
        return $this->run($sql, $bind);
    }

    /**
     * Xóa dữ liệu
     * @param string $table
     * @param string $where
     * @param array $bind
     * @return bool|int
     */
    public function delete($table, $where, $bind = array())
    {
        $sql = "DELETE FROM {$table} WHERE {$where};";
        return $this->run($sql, $bind);
    }

    public function disconnect()
    {
        $this->_instance = null;
    }
}